/*
# pymoz, Python extension for the Mozilla Toolkit
# Copyright (C) 2012 Malek Hadj-Ali
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/


#include "pyRuntime.h"

#include "mozilla/ModuleUtils.h"
#include "prlink.h"

#pragma GCC visibility push(default)
#include "structmember.h"
#pragma GCC visibility pop


static PRLibrary *gPythonLib = NULL;


/*******************************************************************************
* global utils
*******************************************************************************/

PyObject *
Py_GetGlobals()
{
    PyObject *globals = PyEval_GetGlobals(); // borrowed
    if (!globals) {
        PyObject *pymain = PyImport_AddModule("__main__"); // borrowed
        if (pymain) {
            globals = PyModule_GetDict(pymain); // borrowed
        }
    }
    return globals;
}


PyObject *
PyModule_GetAttrId(const char *modname, _Py_Identifier *attrid)
{
    PyObject *module = PyImport_ImportModule(modname);
    if (!module) {
        return NULL;
    }
    PyObject *result = _PyObject_GetAttrId(module, attrid);
    Py_DECREF(module);
    return result;
}


int
PyModule_SetAttrId(const char *modname, _Py_Identifier *attrid, PyObject *value)
{
    PyObject *module = PyImport_ImportModule(modname);
    if (!module) {
        return -1;
    }
    int result = _PyObject_SetAttrId(module, attrid, value);
    Py_DECREF(module);
    return result;
}

int
PyModule_AddPyObject(PyObject *module, const char *name, PyObject *pyobj)
{
    Py_INCREF(pyobj);
    if (PyModule_AddObject(module, name, pyobj)) {
        Py_DECREF(pyobj);
        return -1;
    }
    return 0;
}


/*******************************************************************************
 utils
*******************************************************************************/

static PyObject *
PyWarn_Display(PyObject *message, PyObject *category, PyObject *filename,
               PyObject *lineno, PyObject *file, PyObject *line)
{
    _Py_IDENTIFIER(__showwarning__);

    PyObject *__showwarning__ = PyModule_GetAttrId("warnings",
                                                   &PyId___showwarning__);
    if (!__showwarning__) {
        return NULL;
    }
    PyObject *result = PyObject_CallFunctionObjArgs(__showwarning__,
                                                    message, category, filename,
                                                    lineno, file, line, NULL);
    Py_DECREF(__showwarning__);
    return result;
}


/*******************************************************************************
* PyComponentMetaType
*******************************************************************************/

/* PyComponentMetaType.tp_dealloc */
static void
PyComponentMeta_tp_dealloc(PyObject *self)
{
    PyType_Type.tp_dealloc(self);
}


/* PyComponentMetaType.tp_new */
static PyObject *
PyComponentMeta_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PyObject *self = NULL;
    PyObject *interfaces = NULL, *QueryInterface = NULL;
    _Py_IDENTIFIER(interfaces);
    _Py_IDENTIFIER(QueryInterface);
    _Py_IDENTIFIER(generateQI);

    self = PyType_Type.tp_new(type, args, kwargs);
    if (!self) {
        return NULL;
    }

    // interfaces
    interfaces = _PyObject_GetAttrId(self, &PyId_interfaces);
    if (!interfaces) {
        goto fail;
    }
    if (!PyList_Check(interfaces)) {
        PyErr_SetString(PyExc_TypeError,
                        "class attribute 'interfaces' must be a list");
        goto fail;
    }
    QueryInterface =
        _PyObject_CallMethodObjIdArgs(pyRuntime::sPyRuntime->mXPCOMUtils,
                                      &PyId_generateQI, interfaces, NULL);
    if (!QueryInterface ||
        _PyObject_SetAttrId(self, &PyId_QueryInterface, QueryInterface)) {
        goto fail;
    }

    goto finish;

fail:
    Py_CLEAR(self);

finish:
    Py_XDECREF(QueryInterface);
    Py_XDECREF(interfaces);

    return self;
}


/* PyComponentMeta.prototype */
PyDoc_STRVAR(PyComponentMeta_prototype_doc,
"prototype");

static PyObject *
PyComponentMeta_prototype_get(PyObject *self, void *closure)
{
    Py_INCREF(self);
    return self;
}


/* PyComponentMeta._xpcom_factory */
PyDoc_STRVAR(PyComponentMeta__xpcom_factory_doc,
"_xpcom_factory");

static PyObject *
PyComponentMeta__xpcom_factory_get(PyObject *self, void *closure)
{
    Py_RETURN_NONE;
}


/* PyComponentMetaType.tp_getsets */
static PyGetSetDef PyComponentMeta_tp_getsets[] = {
    {"prototype", (getter)PyComponentMeta_prototype_get, NULL,
     PyComponentMeta_prototype_doc, NULL},
    {"_xpcom_factory", (getter)PyComponentMeta__xpcom_factory_get, NULL,
     PyComponentMeta__xpcom_factory_doc, NULL},
    {NULL}  /* Sentinel */
};


/* PyComponentMetaType */
static PyTypeObject PyComponentMetaType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "pymoz.PyComponentMeta",                  /*tp_name*/
    sizeof(PyHeapTypeObject),                 /*tp_basicsize*/
    sizeof(PyMemberDef),                      /*tp_itemsize*/
    (destructor)PyComponentMeta_tp_dealloc,   /*tp_dealloc*/
    0,                                        /*tp_print*/
    0,                                        /*tp_getattr*/
    0,                                        /*tp_setattr*/
    0,                                        /*tp_compare*/
    0,                                        /*tp_repr*/
    0,                                        /*tp_as_number*/
    0,                                        /*tp_as_sequence*/
    0,                                        /*tp_as_mapping*/
    0,                                        /*tp_hash*/
    0,                                        /*tp_call*/
    0,                                        /*tp_str*/
    0,                                        /*tp_getattro*/
    0,                                        /*tp_setattro*/
    0,                                        /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_TYPE_SUBCLASS, /*tp_flags*/
    0,                                        /*tp_doc*/
    0,                                        /*tp_traverse*/
    0,                                        /*tp_clear*/
    0,                                        /*tp_richcompare*/
    0,                                        /*tp_weaklistoffset*/
    0,                                        /*tp_iter*/
    0,                                        /*tp_iternext*/
    0,                                        /*tp_methods*/
    0,                                        /*tp_members*/
    PyComponentMeta_tp_getsets,               /*tp_getsets*/
    0,                                        /*tp_base*/
    0,                                        /*tp_dict*/
    0,                                        /*tp_descr_get*/
    0,                                        /*tp_descr_set*/
    0,                                        /*tp_dictoffset*/
    0,                                        /*tp_init*/
    0,                                        /*tp_alloc*/
    PyComponentMeta_tp_new,                   /*tp_new*/
};


/*******************************************************************************
* PyComponentType
*******************************************************************************/

typedef struct PyComponent : PyObject {
    PyObject *weakrefslist;
} PyComponent;


/* PyComponentType.tp_dealloc */
static void
PyComponent_tp_dealloc(PyComponent *self)
{
    if (self->weakrefslist) {
        PyObject_ClearWeakRefs(self);
    }

    Py_TYPE(self)->tp_free(self);
}


/* PyComponentType.tp_new */
static PyObject *
new_PyComponent(PyTypeObject *type)
{
    PyComponent *self = (PyComponent *)type->tp_alloc(type, 0);
    if (!self) {
        return NULL;
    }
    self->weakrefslist = NULL;
    return (PyObject *)self;
}


static PyObject *
PyComponent_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
    PyObject *self = NULL, *service = NULL;
    _Py_IDENTIFIER(service);
    _Py_IDENTIFIER(_instance);

    service = _PyObject_GetAttrId((PyObject *)type, &PyId_service);
    if (!service) {
        return NULL;
    }
    if (service == Py_True) {
        if (_PyObject_HasAttrId((PyObject *)type, &PyId__instance)) {
            self = _PyObject_GetAttrId((PyObject *)type, &PyId__instance);
        }
        else {
            self = new_PyComponent(type);
            if (self &&
                _PyObject_SetAttrId((PyObject *)type, &PyId__instance, self)) {
                Py_CLEAR(self);
            }
        }
    }
    else {
        self = new_PyComponent(type);
    }
    Py_DECREF(service);

    return self;
}


/* PyComponent.wrappedJSObject */
PyDoc_STRVAR(PyComponent_wrappedJSObject_doc,
"wrappedJSObject");

static PyObject *
PyComponent_wrappedJSObject_get(PyComponent *self, void *closure)
{
    Py_INCREF(self);
    return self;
}


/* PyComponentType.tp_getsets */
static PyGetSetDef PyComponent_tp_getsets[] = {
    {"wrappedJSObject", (getter)PyComponent_wrappedJSObject_get, NULL,
     PyComponent_wrappedJSObject_doc, NULL},
    {NULL}  /* Sentinel */
};


/* PyComponentType */
static PyTypeObject PyComponentType = {
    PyVarObject_HEAD_INIT(&PyComponentMetaType, 0)
    "pymoz.PyComponent",                      /*tp_name*/
    sizeof(PyComponent),                      /*tp_basicsize*/
    0,                                        /*tp_itemsize*/
    (destructor)PyComponent_tp_dealloc,       /*tp_dealloc*/
    0,                                        /*tp_print*/
    0,                                        /*tp_getattr*/
    0,                                        /*tp_setattr*/
    0,                                        /*tp_compare*/
    0,                                        /*tp_repr*/
    0,                                        /*tp_as_number*/
    0,                                        /*tp_as_sequence*/
    0,                                        /*tp_as_mapping*/
    0,                                        /*tp_hash*/
    0,                                        /*tp_call*/
    0,                                        /*tp_str*/
    0,                                        /*tp_getattro*/
    0,                                        /*tp_setattro*/
    0,                                        /*tp_as_buffer*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
    0,                                        /*tp_doc*/
    0,                                        /*tp_traverse*/
    0,                                        /*tp_clear*/
    0,                                        /*tp_richcompare*/
    offsetof(PyComponent, weakrefslist),      /*tp_weaklistoffset*/
    0,                                        /*tp_iter*/
    0,                                        /*tp_iternext*/
    0,                                        /*tp_methods*/
    0,                                        /*tp_members*/
    PyComponent_tp_getsets,                   /*tp_getsets*/
    0,                                        /*tp_base*/
    0,                                        /*tp_dict*/
    0,                                        /*tp_descr_get*/
    0,                                        /*tp_descr_set*/
    0,                                        /*tp_dictoffset*/
    0,                                        /*tp_init*/
    0,                                        /*tp_alloc*/
    PyComponent_tp_new,                       /*tp_new*/
};


/*******************************************************************************
 pymoz_module
*******************************************************************************/

/* pymoz_module.m_doc */
PyDoc_STRVAR(pymoz_m_doc,
"pymoz module");


/* pymoz.excepthook(type, value, traceback) */
PyDoc_STRVAR(pymoz_excepthook_doc,
"excepthook(type, value, traceback)");

static PyObject *
pymoz_excepthook(PyObject *module, PyObject *args)
{
    PyObject *exctype, *excvalue, *exctraceback;

    if (!PyArg_ParseTuple(args, "OOO:excepthook",
                          &exctype, &excvalue, &exctraceback)) {
        return NULL;
    }
    PyErr_Display(exctype, excvalue, exctraceback);
    if (!pyRuntime::sPyRuntime->SetException(excvalue)) {
        return NULL;
    }
    Py_RETURN_NONE;
}


/* pymoz.showwarning(message, category, filename, lineno[, file=None, line=None]) */
PyDoc_STRVAR(pymoz_showwarning_doc,
"showwarning(message, category, filename, lineno[, file=None, line=None])");

static PyObject *
pymoz_showwarning(PyObject *module, PyObject *args)
{
    PyObject *message, *category, *filename, *lineno;
    PyObject *file = Py_None, *line = Py_None;

    if (!PyArg_ParseTuple(args, "OOOO|OO:showwarning",
                          &message, &category, &filename, &lineno,
                          &file, &line)) {
        return NULL;
    }
    PyObject *tmp = PyWarn_Display(message, category, filename, lineno,
                                   file, line);
    if (!tmp) {
        return NULL;
    }
    Py_DECREF(tmp);
    if (!pyRuntime::sPyRuntime->ReportWarning(message, category, filename,
                                              lineno, line)) {
        return NULL;
    }
    Py_RETURN_NONE;
}


/* pymoz.js_importmodule(uri[, target=__main__]) */
PyDoc_STRVAR(pymoz_js_importmodule_doc,
"js_importmodule(uri[, target=__main__])");

static PyObject *
pymoz_js_importmodule(PyObject *module, PyObject *args)
{
    PyObject *pyuri = NULL;
    PyObject *target = PyImport_AddModule("__main__"); // borrowed

    if (!target) {
        return NULL;
    }

    if (!PyArg_ParseTuple(args, "O&|O!:js_importmodule",
                          PyUnicode_FSConverter, &pyuri,
                          &PyModule_Type, &target)) {
        return NULL;
    }

    const char *uri = PyBytes_AsString(pyuri);
    if (!uri) {
        return NULL;
    }

    PyObject *result = pyRuntime::sPyRuntime->JSImportModule(uri, target);
    if (!result) {
        return NULL;
    }
    Py_DECREF(result);

    Py_RETURN_NONE;
}


/* pymoz.js_getglobal() -> current js glogal object (window) */
PyDoc_STRVAR(pymoz_js_getglobal_doc,
"js_getglobal() -> current js glogal object (window)");

static PyObject *
pymoz_js_getglobal(PyObject *module)
{
    return pyRuntime::sPyRuntime->JSGetGlobal();
}


/* pymoz.js_instanceof(obj, proto) -> bool*/
PyDoc_STRVAR(pymoz_js_instanceof_doc,
"js_instanceof(obj, proto) -> bool");

static PyObject *
pymoz_js_instanceof(PyObject *module, PyObject *args)
{
    PyObject *obj, *proto;

    if (!PyArg_ParseTuple(args, "OO:js_instanceof", &obj, &proto)) {
        return NULL;
    }

    return pyRuntime::sPyRuntime->JSInstanceOf(obj, proto);
}


/* pymoz_module.m_methods */
static PyMethodDef pymoz_m_methods[] = {
    {"__excepthook__", (PyCFunction)pymoz_excepthook, METH_VARARGS,
     pymoz_excepthook_doc},
    {"excepthook", (PyCFunction)pymoz_excepthook, METH_VARARGS,
     pymoz_excepthook_doc},
    {"__showwarning__", (PyCFunction)pymoz_showwarning, METH_VARARGS,
     pymoz_showwarning_doc},
    {"showwarning", (PyCFunction)pymoz_showwarning, METH_VARARGS,
     pymoz_showwarning_doc},
    {"js_importmodule", (PyCFunction)pymoz_js_importmodule, METH_VARARGS,
     pymoz_js_importmodule_doc},
    {"js_getglobal", (PyCFunction)pymoz_js_getglobal, METH_NOARGS,
     pymoz_js_getglobal_doc},
    {"js_instanceof", (PyCFunction)pymoz_js_instanceof, METH_VARARGS,
     pymoz_js_instanceof_doc},
    {NULL} /* Sentinel */
};


/* pymoz_module */
static PyModuleDef pymoz_module = {
    PyModuleDef_HEAD_INIT,
    "pymoz",                                  /*m_name*/
    pymoz_m_doc,                              /*m_doc*/
    -1,                                       /*m_size*/
    pymoz_m_methods,                          /*m_methods*/
};


/* pymoz_module initialization */
PyMODINIT_FUNC
PyInit_pymoz(void)
{
    /* PyComponentMetaType */
    PyComponentMetaType.tp_base = &PyType_Type;
    if (PyType_Ready(&PyComponentMetaType)) {
        return NULL;
    }

    /* PyComponentType */
    PyObject *base_dict = PyDict_New();
    if (!base_dict) {
        return NULL;
    }
    PyObject *base_ifaces = PyList_New(0);
    if (!base_ifaces) {
        Py_DECREF(base_dict);
        return NULL;
    }
    if (PyDict_SetItemString(base_dict, "interfaces", base_ifaces) ||
        PyDict_SetItemString(base_dict, "service", Py_False)) {
        Py_DECREF(base_ifaces);
        Py_DECREF(base_dict);
        return NULL;
    }
    Py_DECREF(base_ifaces);
    PyComponentType.tp_dict = base_dict; // will be decrefed with the type dealloc
    if (PyType_Ready(&PyComponentType)) {
        return NULL;
    }

    /* pymoz */
    PyObject *pymoz = PyModule_Create(&pymoz_module);
    if (pymoz &&
        PyModule_AddPyObject(pymoz, "PyComponent", (PyObject *)&PyComponentType)) {
        Py_CLEAR(pymoz);
    }
    return pymoz;
}


/*******************************************************************************
* mozilla Module implementation
*******************************************************************************/

NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(pyRuntime, pyRuntime::GetService)
NS_DEFINE_NAMED_CID(PY_RUNTIME_CID);


static const mozilla::Module::CIDEntry kPythonCIDs[] = {
    { &kPY_RUNTIME_CID, true, NULL, pyRuntimeConstructor },
    { NULL }
};


static const mozilla::Module::ContractIDEntry kPythonContractIDs[] = {
    { PY_RUNTIME_CONTRACTID, &kPY_RUNTIME_CID },
    { NULL }
};


static const mozilla::Module::CategoryEntry kPythonCategories[] = {
    { "module-loader", "py", PY_RUNTIME_CONTRACTID },
    { "agent-style-sheets", "Python bindings",
      "chrome://pymoz/content/pymoz.css" },
    { NULL }
};


/* Load/Unload */

static nsresult
LoadPython()
{
    char *libname = PR_GetLibraryName(NULL, PYTHON_LIBNAME);
    if (!libname) {
        return NS_ERROR_OUT_OF_MEMORY;
    }
    PRLibSpec libspec;
    libspec.type = PR_LibSpec_Pathname;
    libspec.value.pathname = libname;
    gPythonLib = PR_LoadLibraryWithFlags(libspec, PR_LD_LAZY | PR_LD_GLOBAL);
    PR_FreeLibraryName(libname);
    if (!gPythonLib) {
        return NS_ERROR_FAILURE;
    }
    if (PyImport_AppendInittab("pymoz", &PyInit_pymoz)) {
        return NS_ERROR_FAILURE;
    }
    Py_Initialize();
    if (!Py_IsInitialized()) {
        return NS_ERROR_FAILURE;
    }
    return NS_OK;
}


static void
UnloadPython()
{
    if (gPythonLib) {
        if (Py_IsInitialized()) {
            Py_Finalize();
        }
        PR_UnloadLibrary(gPythonLib);
        gPythonLib = NULL;
    }
}


/* PythonModule */
static const mozilla::Module kPythonModule = {
    mozilla::Module::kVersion,
    kPythonCIDs,
    kPythonContractIDs,
    kPythonCategories,
    NULL,
    LoadPython,
    UnloadPython
};


NSMODULE_DEFN(PythonModule) = &kPythonModule;
